package animation;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;

/**
 * A tree used to combine animations.
 */
public class AnimationTree {
	public AnimationTree() {
		root = null;
		nodeNames = new HashMap<String, Node>();
	}

	public void save(Path path)
			throws IOException {
		Writer writer = null;
		try {
			JSONObject rootObj = new JSONObject();

			JSONArray treeArr = new JSONArray();
			rootObj.put("tree", treeArr);

			if (root != null) {
				root.save(treeArr);
			}

			writer = Files.newBufferedWriter(path, Charset.defaultCharset());
			rootObj.write(writer);
		} catch (JSONException e) {
			System.out.println("Error: " + e.getMessage());
			throw new IOException("Failed to write " + path);
		} finally {
			if (writer != null) {
				try {
					writer.close();
				} catch (IOException e) {}
			}
		}
	}

	public void load(Path path)
			throws IOException {
		root = null;
		Reader reader = null;
		try {
			reader = Files.newBufferedReader(path, Charset.defaultCharset());
			JSONObject rootObj = new JSONObject(new JSONTokener(reader));

			JSONArray treeArr = rootObj.getJSONArray("tree");
			if (treeArr.length() > 1) {
				throw new IOException("At most one root element should be specified");
			}
			root = loadNodes(treeArr.getJSONObject(0));
		} catch (JSONException e) {
			System.out.println("Error: " + e.getMessage());
			throw new IOException("Failed to read " + path);
		} catch (IOException e) {
			System.out.println("Error: " + e.getMessage());
			throw new IOException("Failed to read " + path);
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {}
			}
		}
	}

	private Node loadNodes(JSONObject nodeObj)
			throws IOException {
		// read the node type
		String typeStr = nodeObj.getString("type");
		String name = nodeObj.getString("name");
		if (nodeNames.containsKey(name)) {
			throw new IOException("Duplicate node " + name);
		}
		Node n;
		if (typeStr.equals(Node.Type.ANIMATION.toString())) {
			n = new AnimationNode(name);
		} else if (typeStr.equals(Node.Type.SELECT.toString())) {
			n = new SelectNode(name);
		} else if (typeStr.equals(Node.Type.BLEND.toString())) {
			n = new BlendNode(name);
		} else if (typeStr.equals(Node.Type.SEQUENCE.toString())) {
			n = new SequenceNode(name);
		} else {
			throw new IOException("Invalid node type " + typeStr);
		}

		n.addToTree(this);
		n.loadData(nodeObj);
		return n;
	}

	// a node in the animation tree - can be one of several types
	public abstract static class Node {
		public enum Type {
			ANIMATION,
			SELECT,
			BLEND,
			SEQUENCE
		}

		private Node(Type type, String name) {
			this.type = type;
			this.name = name;
			tree = null;
		}

		public Type getType() {
			return type;
		}

		public String getName() {
			return name;
		}

		public void setName(String name) {
			if (tree != null) {
				// need to first check if tree already contains a node with this name
				Node n = tree.getNode(name);
				if (n != null && n != this) {
					throw new IllegalArgumentException("Node " + name + " already exists");
				}
				tree.nodeNames.remove(this.name);
				tree.nodeNames.put(name, this);
			}
			this.name = name;
		}

		public Node getParent() {
			return parent;
		}

		public abstract int getChildCount();
		public abstract Node getChild(int index);
		public abstract void addChild(int index, Node node);
		public abstract void removeChild(int index);

		protected AnimationTree getTree() {
			return tree;
		}

		protected void addToTree(AnimationTree tree) {
			if (this.tree != null) {
				throw new IllegalArgumentException("Node already in tree");
			}
			if (tree.nodeNames.containsKey(name)) {
				throw new IllegalArgumentException("Node " + name + " already exists");
			}
			tree.nodeNames.put(name, this);
			this.tree = tree;
			addChildrenToTree();
		}

		protected void removeFromTree() {
			if (tree == null) {
				throw new IllegalArgumentException("Node not in tree");
			}
			tree.nodeNames.remove(name);
			tree = null;
			removeChildrenFromTree();
		}

		protected abstract void removeChildrenFromTree();
		protected abstract void addChildrenToTree();

		protected void setParent(Node parent) {
			this.parent = parent;
		}

		private Type type;
		private String name;
		private AnimationTree tree;
		private Node parent;

		protected void save(JSONArray nodesArr) {
			JSONObject nodeObj = new JSONObject();
			nodesArr.put(nodeObj);

			nodeObj.put("type", getType().toString());
			nodeObj.put("name", getName());
			saveData(nodeObj);
		}

		protected abstract void saveData(JSONObject nodeObj);
		protected abstract void loadData(JSONObject nodeObj) throws IOException;
	}

	// animation node - used to play an animation
	public static class AnimationNode extends Node {
		public AnimationNode(String name) {
			super(Type.ANIMATION, name);
		}

		public void setAnimation(String animation) {
			this.animation = animation;
		}

		public String getAnimation() {
			return animation;
		}

		public int getChildCount() {
			return 0;
		}

		public Node getChild(int index) {
			throw new UnsupportedOperationException("Animation node does not support children");
		}

		public void addChild(int index, Node child) {
			throw new UnsupportedOperationException("Animation node does not support children");
		}

		public void removeChild(int index) {
			throw new UnsupportedOperationException("Animation node does not support children");
		}

		private String animation;

		protected void removeChildrenFromTree() {
		}

		protected void addChildrenToTree() {
		}

		protected void saveData(JSONObject nodeObj) {
			nodeObj.put("animation", animation);
		}

		protected void loadData(JSONObject nodeObj)
				throws IOException {
			setAnimation(nodeObj.getString("animation"));
		}
	}

	// select node - used to select one of several children nodes
	public static class SelectNode extends Node {
		public SelectNode(String name) {
			super(Type.SELECT, name);
			children = new ArrayList<Node>();
		}

		public int getChildCount() {
			return children.size();
		}

		public Node getChild(int index) {
			return children.get(index);
		}

		public void addChild(int index, Node child) {
			if (child.getParent() != null) {
				throw new IllegalArgumentException("Node already has a parent");
			}
			child.setParent(this);
			children.add(index, child);
			if (getTree() != null) {
				child.addToTree(getTree());
			}
		}

		public void removeChild(int index) {
			Node child = children.remove(index);
			child.setParent(null);
			if (getTree() != null) {
				child.removeFromTree();
			}
		}

		private ArrayList<Node> children;

		protected void removeChildrenFromTree() {
			for (Node child : children) {
				child.removeFromTree();
			}
		}

		protected void addChildrenToTree() {
			for (Node child : children) {
				child.addToTree(getTree());
			}
		}

		protected void saveData(JSONObject nodeObj) {
			JSONArray childrenArr = new JSONArray();
			nodeObj.put("children", childrenArr);

			for (Node child : children) {
				child.save(childrenArr);
			}
		}

		protected void loadData(JSONObject nodeObj)
				throws IOException {
			// get children
			JSONArray childrenArr = nodeObj.getJSONArray("children");
			for (int c = 0; c < childrenArr.length(); ++c) {
				children.add(getTree().loadNodes(childrenArr.getJSONObject(c)));
			}
		}
	}

	// blend node - used to blend between two of n nodes
	// wrap indicates whether nodes (n-1) and 0 should blend (true), or clamp (false)
	public static class BlendNode extends Node {
		public BlendNode(String name) {
			super(Type.BLEND, name);
			wrap = false;
			children = new ArrayList<Node>();
		}

		public void setWrap(boolean wrap) {
			this.wrap = wrap;
		}

		public boolean getWrap() {
			return wrap;
		}

		public int getChildCount() {
			return children.size();
		}

		public Node getChild(int index) {
			return children.get(index);
		}

		public void addChild(int index, Node child) {
			if (child.getParent() != null) {
				throw new IllegalArgumentException("Node already has a parent");
			}
			child.setParent(this);
			children.add(index, child);
			if (getTree() != null) {
				child.addToTree(getTree());
			}
		}

		public void removeChild(int index) {
			Node child = children.remove(index);
			child.setParent(null);
			if (getTree() != null) {
				child.removeFromTree();
			}
		}

		private boolean wrap;
		private ArrayList<Node> children;

		protected void removeChildrenFromTree() {
			for (Node child : children) {
				child.removeFromTree();
			}
		}

		protected void addChildrenToTree() {
			for (Node child : children) {
				child.addToTree(getTree());
			}
		}

		protected void saveData(JSONObject nodeObj) {
			nodeObj.put("wrap", wrap);

			JSONArray childrenArr = new JSONArray();
			nodeObj.put("children", childrenArr);

			for (Node child : children) {
				child.save(childrenArr);
			}
		}

		protected void loadData(JSONObject nodeObj)
				throws IOException {
			wrap = nodeObj.getBoolean("wrap");

			// get children
			JSONArray childrenArr = nodeObj.getJSONArray("children");
			for (int c = 0; c < childrenArr.length(); ++c) {
				children.add(getTree().loadNodes(childrenArr.getJSONObject(c)));
			}
		}
	}

	// executes child nodes in sequence
	public static class SequenceNode extends Node {
		public SequenceNode(String name) {
			super(Type.SEQUENCE, name);
			children = new ArrayList<Node>();
		}

		public int getChildCount() {
			return children.size();
		}

		public Node getChild(int index) {
			return children.get(index);
		}

		public void addChild(int index, Node child) {
			if (child.getParent() != null) {
				throw new IllegalArgumentException("Node already has a parent");
			}
			child.setParent(this);
			children.add(index, child);
			if (getTree() != null) {
				child.addToTree(getTree());
			}
		}

		public void removeChild(int index) {
			Node child = children.remove(index);
			child.setParent(null);
			if (getTree() != null) {
				child.removeFromTree();
			}
		}

		private ArrayList<Node> children;

		protected void removeChildrenFromTree() {
			for (Node child : children) {
				child.removeFromTree();
			}
		}

		protected void addChildrenToTree() {
			for (Node child : children) {
				child.addToTree(getTree());
			}
		}

		protected void saveData(JSONObject nodeObj) {
			JSONArray childrenArr = new JSONArray();
			nodeObj.put("children", childrenArr);

			for (Node child : children) {
				child.save(childrenArr);
			}
		}

		protected void loadData(JSONObject nodeObj)
				throws IOException {
			// get children
			JSONArray childrenArr = nodeObj.getJSONArray("children");
			for (int c = 0; c < childrenArr.length(); ++c) {
				children.add(getTree().loadNodes(childrenArr.getJSONObject(c)));
			}
		}
	}

	public void setRoot(Node root) {
		if (root.getParent() != null) {
			throw new IllegalArgumentException("Node already has a parent");
		}
		if (this.root != null) {
			this.root.removeFromTree();
		}
		root.setParent(null);
		if (root != null) {
			root.addToTree(this);
		}
		this.root = root;
	}

	public Node getRoot() {
		return root;
	}

	public Node getNode(String name) {
		return nodeNames.get(name);
	}

	private Node root;

	private HashMap<String, Node> nodeNames;
}
